// Copyright 2011 The Apache Software Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package org.apache.tapestry5.csrfprotection.services;

import org.apache.tapestry5.Link;
import org.apache.tapestry5.csrfprotection.CsrfProtectionClassTransformWorker;
import org.apache.tapestry5.csrfprotection.CsrfProtectionFilterImpl;
import org.apache.tapestry5.csrfprotection.CsrfTokenProvider;
import org.apache.tapestry5.ioc.Configuration;
import org.apache.tapestry5.ioc.MappedConfiguration;
import org.apache.tapestry5.ioc.ObjectLocator;
import org.apache.tapestry5.ioc.OrderedConfiguration;
import org.apache.tapestry5.ioc.ServiceBinder;
import org.apache.tapestry5.ioc.annotations.Contribute;
import org.apache.tapestry5.ioc.services.FactoryDefaults;
import org.apache.tapestry5.ioc.services.SymbolProvider;
import org.apache.tapestry5.services.ApplicationStateManager;
import org.apache.tapestry5.services.ComponentEventLinkEncoder;
import org.apache.tapestry5.services.ComponentEventRequestFilter;
import org.apache.tapestry5.services.ComponentEventRequestHandler;
import org.apache.tapestry5.services.ComponentEventRequestParameters;
import org.apache.tapestry5.services.LibraryMapping;
import org.apache.tapestry5.services.PageRenderRequestParameters;
import org.apache.tapestry5.services.Request;
import org.apache.tapestry5.services.transform.ComponentClassTransformWorker2;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * This module contains logic to protect a Tapestry based application against
 * cross-site request forgery (CSRF) attacks. This is done by adding a
 * secure token to each form and event link and comparing the client side token
 * to a server side stored token. It offers an explicit mode, which allows to
 * add the CSRF protection based on mixins and a protection annotation for
 * methods or pages. The auto mode adds it automatically to all form or link
 * based actions and uses a filter based protection mechanism.
 */

public class CsrfProtectionModule {	
	/* Configuration parameters */
	
	/**
	 * Key value for the configuration mode.
	 */
	public static final String ANTI_CSRF_MODE = "csrfprotection.antiCsrfMode";

	/**
	 * Configuration value for the ANTI_CSRF_MODE. The auto mode 
	 * uses a filters and decorators to provide the anti CSRF functionality.
	 */
	public static final String ANTI_CSRF_MODE_AUTO = "auto";
	
	/**
	 * Configuration value for the ANTI_CSRF_MODE. In the explicit mode
	 * you have to use the mixin.
	 */
	public static final String ANTI_CSRF_MODE_EXPLICIT = "explicit";
	
	/**
	 * Configuration value for the ANTI_CSRF_MODE. In the off mode there
	 * is no class transformation performed. The mixin and the annotation
	 * are not available then.
	 */
	public static final String ANTI_CSRF_MODE_OFF = "off";

	/**
	 * Key value for the token type configuration.
	 */
	public static final String ANTI_CSRF_TOKENTYPE = "csrfprotection.tokenType";

	/**
	 * Session based token.
	 */
	public static final String ANTI_CSRF_TOKENTYPE_SESSION = "session";

	/**
	 * Key value for the token persistence type.
	 */
	public static final String ANTI_CSRF_TOKENPERSISTENCE = "csrfprotection.tokenPersistence";

	/**
	 * Configuration value for the token persistence type. The serverside mode holds the
	 * token in the session state.
	 */
	public static final String ANTI_CSRF_TOKENPERSISTENCE_SERVERSIDE = "serverSide";	

	private static Logger logger = LoggerFactory
			.getLogger(CsrfProtectionModule.class);

	/**
	 * Bind services.
	 * @param binder
	 */
	public static void bind(ServiceBinder binder) {
		binder.bind(CsrfProtectedPages.class);
		binder.bind(DynamicConfig.class);		
	}

	/**
	 * The core functionality is class transformation.
	 * @param configuration
	 */
	@Contribute(ComponentClassTransformWorker2.class)
	public static void contributeComponentClassTransformWorker2(
			OrderedConfiguration<ComponentClassTransformWorker2> configuration) {
		logger.debug("Providing CsrfProtectionClassTransformWorker.");
		configuration.addInstance("Protected",
				CsrfProtectionClassTransformWorker.class);
	}	

	/**
	 * The configuration defaults for the CSRF protection module are configured here.
	 * @param configuration
	 */
	@Contribute(SymbolProvider.class)
	@FactoryDefaults
	public static void contributeFactoryDefaults(
			MappedConfiguration<String, String> configuration) {
		logger.debug("Contributing factory defaults - csrf protection module.");
		configuration.add(ANTI_CSRF_MODE, ANTI_CSRF_MODE_EXPLICIT);
		configuration.add(ANTI_CSRF_TOKENTYPE, ANTI_CSRF_TOKENTYPE_SESSION);
		configuration.add(ANTI_CSRF_TOKENPERSISTENCE,
				ANTI_CSRF_TOKENPERSISTENCE_SERVERSIDE);
	}

	/**
	 * Contributed component class resolver.
	 * @param configuration
	 */
	public static void contributeComponentClassResolver(
			Configuration<LibraryMapping> configuration) {

		configuration.add(new LibraryMapping("csrfprotection",
				"org.apache.tapestry5.csrfprotection"));
	}

	/**
	 * Contributed event request handler.
	 * @param configuration
	 */
	@Contribute(ComponentEventRequestHandler.class)
	public void contributeComponentEventRequestHandler(
			OrderedConfiguration<ComponentEventRequestFilter> configuration) {
		configuration.addInstance("CsrfProtectionFilter",
				CsrfProtectionFilterImpl.class, "after:*");

	}
	
	/**
	 * Decorates the generated event links for the auto mode of the 
	 * cross-site request forgery protection. Only active if the auto mode
	 * is configured.
	 * @param delegate
	 * @param objectLocator
	 * @return
	 */
	public ComponentEventLinkEncoder decorateComponentEventLinkEncoder(
			final ComponentEventLinkEncoder delegate, final ObjectLocator objectLocator) {
		return new ComponentEventLinkEncoder() {
			
			public Link createPageRenderLink(
					PageRenderRequestParameters parameters) {

				return delegate.createPageRenderLink(parameters);
			}
		
			public Link createComponentEventLink(
					ComponentEventRequestParameters parameters,	boolean forForm) {				
				ApplicationStateManager asm = objectLocator.getService(ApplicationStateManager.class);
				DynamicConfig dynConf = objectLocator.getService(DynamicConfig.class);
				CsrfTokenProvider tokenProvider = asm.get(CsrfTokenProvider.class);
				CsrfProtectedPages csrfProtectedPages = objectLocator.getService(CsrfProtectedPages.class);				
				Link link = delegate.createComponentEventLink(parameters,
						forForm);
				// If auto mode is used, add the token to all generated component event links.
				// If the page is marked as unproteted, do not add the token.
				if((ANTI_CSRF_MODE_AUTO.equals(dynConf.getAntiCsrfmode()) && !csrfProtectedPages.isUnprotected(parameters.getContainingPageName()))){
					logger.debug("creating component event link. forForm: " + forForm);
					link.addParameter(CsrfTokenProvider.TOKEN_NAME, tokenProvider.getSessionToken());
					logger.debug("link: " + link.toString());
					return link;
				}
				return link;
			}

			public ComponentEventRequestParameters decodeComponentEventRequest(
					Request request) {

				return delegate.decodeComponentEventRequest(request);
			}

			public PageRenderRequestParameters decodePageRenderRequest(
					Request request) {
				return delegate.decodePageRenderRequest(request);
			}
		};
	}	
}
